package com.agora.data.provider;

import android.content.Context;
import android.text.TextUtils;

import androidx.annotation.NonNull;

import com.agora.data.R;
import com.agora.data.model.AgoraRoom;
import com.agora.data.sync.AgoraException;
import com.agora.data.sync.CollectionReference;
import com.agora.data.sync.DocumentReference;
import com.agora.data.sync.FieldFilter;
import com.agora.data.sync.ISyncManager;
import com.agora.data.sync.OrderBy;
import com.agora.data.sync.Query;
import com.agora.data.sync.RoomReference;
import com.agora.data.sync.SyncManager;
import com.google.gson.Gson;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import cn.leancloud.LCException;
import cn.leancloud.LCLogger;
import cn.leancloud.LCObject;
import cn.leancloud.LCQuery;
import cn.leancloud.LeanCloud;
import cn.leancloud.livequery.LCLiveQuery;
import cn.leancloud.livequery.LCLiveQueryEventHandler;
import cn.leancloud.livequery.LCLiveQuerySubscribeCallback;
import cn.leancloud.push.PushService;
import cn.leancloud.types.LCNull;
import io.agora.baselibrary.BuildConfig;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;

public class DataSyncImpl implements ISyncManager {

    private Gson mGson = new Gson();

    public DataSyncImpl(Context mContext) {
        if (BuildConfig.DEBUG) {
            LeanCloud.setLogLevel(LCLogger.Level.DEBUG);
        } else {
            LeanCloud.setLogLevel(LCLogger.Level.ERROR);
        }

        String appid = mContext.getString(R.string.leancloud_app_id);
        String appKey = mContext.getString(R.string.leancloud_app_key);
        String url = mContext.getString(R.string.leancloud_server_url);
        if (TextUtils.isEmpty(appid) || TextUtils.isEmpty(appKey) || TextUtils.isEmpty(url)) {
            throw new NullPointerException("please check \"strings_config.xml\"");
        }

        LeanCloud.initialize(mContext, appid, appKey, url);

        PushService.startIfRequired(mContext);
    }

    @Override
    public Observable<AgoraRoom> creatRoom(AgoraRoom room) {
        LCObject mLCObject = new LCObject(AgoraRoom.TABLE_NAME);
        Map<String, Object> datas = room.toHashMap();
        for (Map.Entry<String, Object> entry : datas.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            mLCObject.put(key, value);
        }
        return mLCObject.saveInBackground()
                .subscribeOn(Schedulers.io())
                .map(new Function<LCObject, AgoraRoom>() {
                    @Override
                    public AgoraRoom apply(@NonNull LCObject LCObject) throws Exception {
                        AgoraRoom mAgoraRoom = mGson.fromJson(LCObject.toJSONObject().toJSONString(), AgoraRoom.class);
                        mAgoraRoom.setId(LCObject.getObjectId());
                        return mAgoraRoom;
                    }
                }).onErrorResumeNext(new Function<Throwable, ObservableSource<? extends AgoraRoom>>() {
                    @Override
                    public ObservableSource<? extends AgoraRoom> apply(@NonNull Throwable throwable) throws Exception {
                        return Observable.error(new AgoraException(throwable));
                    }
                });
    }

    @Override
    public Observable<List<AgoraRoom>> getRooms() {
        Date date = new Date();
        date.setTime(System.currentTimeMillis() - (24 * 60 * 60 * 1000));

        LCQuery<LCObject> mLCQuery = LCQuery.getQuery(AgoraRoom.TABLE_NAME);
        mLCQuery.whereGreaterThanOrEqualTo(AgoraRoom.COLUMN_CREATEDAT, date);
        mLCQuery.orderByDescending(AgoraRoom.COLUMN_CREATEDAT);
        return mLCQuery.findInBackground()
                .subscribeOn(Schedulers.io())
                .map(new Function<List<LCObject>, List<AgoraRoom>>() {
                    @Override
                    public List<AgoraRoom> apply(@NonNull List<LCObject> LCObjects) throws Exception {
                        List<AgoraRoom> rooms = new ArrayList<>();
                        for (LCObject object : LCObjects) {
                            AgoraRoom room = mGson.fromJson(object.toJSONObject().toJSONString(), AgoraRoom.class);
                            room.setId(object.getObjectId());
                            rooms.add(room);
                        }
                        return rooms;
                    }
                })
                .onErrorResumeNext(new Function<Throwable, ObservableSource<? extends List<AgoraRoom>>>() {
                    @Override
                    public ObservableSource<? extends List<AgoraRoom>> apply(@NonNull Throwable throwable) throws Exception {
                        return Observable.error(new AgoraException(throwable));
                    }
                });
    }

    @Override
    public void get(DocumentReference reference, SyncManager.DataItemCallback callback) {
        if (reference instanceof RoomReference) {
            LCQuery<LCObject> mLCQuery = LCQuery.getQuery(AgoraRoom.TABLE_NAME);
            mLCQuery.getInBackground(reference.getId())
                    .subscribe(new Observer<LCObject>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull LCObject LCObject) {
                            callback.onSuccess(new AgoraObject(LCObject));
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            callback.onFail(new AgoraException(e));
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        } else {
            String collectionKey = reference.getParent().getKey();
            LCQuery<LCObject> mLCQuery = LCQuery.getQuery(collectionKey);
            mLCQuery.getInBackground(reference.getId())
                    .subscribe(new Observer<LCObject>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull LCObject LCObject) {
                            callback.onSuccess(new AgoraObject(LCObject));
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            callback.onFail(new AgoraException(e));
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        }
    }

    @Override
    public void get(CollectionReference reference, SyncManager.DataListCallback callback) {
        LCQuery<LCObject> mLCQuery = createLCQuery(reference.getKey(), reference.getQuery());
        mLCQuery.findInBackground()
                .subscribe(new Observer<List<LCObject>>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull List<LCObject> LCObjects) {
                        List<AgoraObject> list = new ArrayList<>();
                        for (LCObject LCObject : LCObjects) {
                            list.add(new AgoraObject(LCObject));
                        }
                        callback.onSuccess(list);
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        callback.onFail(new AgoraException(e));
                    }

                    @Override
                    public void onComplete() {

                    }
                });
    }

    @Override
    public void add(CollectionReference reference, HashMap<String, Object> datas, SyncManager.DataItemCallback callback) {
        String collectionKey = reference.getKey();
        LCObject mLCObject = new LCObject(collectionKey);
        for (Map.Entry<String, Object> entry : datas.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value instanceof DocumentReference) {
                CollectionReference referenceParent = ((DocumentReference) value).getParent();
                LCObject LCObjectItem = mLCObject.createWithoutData(referenceParent.getKey(), ((DocumentReference) value).getId());
                mLCObject.put(key, LCObjectItem);
            } else {
                mLCObject.put(key, value);
            }
        }
        mLCObject.saveInBackground()
                .subscribe(new Observer<LCObject>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull LCObject LCObject) {
                        callback.onSuccess(new AgoraObject(LCObject));
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        callback.onFail(new AgoraException(e));
                    }

                    @Override
                    public void onComplete() {

                    }
                });
    }

    @Override
    public void delete(DocumentReference reference, SyncManager.Callback callback) {
        if (reference instanceof RoomReference) {
            LCObject mLCObject = LCObject.createWithoutData(AgoraRoom.TABLE_NAME, reference.getId());
            mLCObject.deleteInBackground()
                    .subscribe(new Observer<LCNull>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull LCNull LCNull) {
                            callback.onSuccess();
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            callback.onFail(new AgoraException(e));
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        } else {
            String collectionKey = reference.getParent().getKey();
            LCObject mLCObjectCollection = LCObject.createWithoutData(collectionKey, reference.getId());
            mLCObjectCollection.deleteInBackground()
                    .subscribe(new Observer<LCNull>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull LCNull LCNull) {
                            callback.onSuccess();
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            callback.onFail(new AgoraException(e));
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        }
    }

    private LCQuery<LCObject> createLCQuery(String theClassName, Query mQuery) {
        LCQuery<LCObject> mLCQuery = LCQuery.getQuery(theClassName);

        if (mQuery != null) {
            List<FieldFilter> list = mQuery.getFilters();
            for (FieldFilter filter : list) {
                if (filter.getOperator() == FieldFilter.Operator.EQUAL) {
                    String field = filter.getField();
                    Object value = filter.getValue();
                    if (value instanceof DocumentReference) {
                        CollectionReference referenceParent = ((DocumentReference) value).getParent();
                        LCObject LCObjectItem = LCObject.createWithoutData(referenceParent.getKey(), ((DocumentReference) value).getId());
                        mLCQuery.whereEqualTo(field, LCObjectItem);
                    } else {
                        mLCQuery.whereEqualTo(field, value);
                    }
                }
            }

            List<OrderBy> orderByList = mQuery.getOrderByList();
            for (OrderBy item : orderByList) {
                if (item.getDirection() == OrderBy.Direction.ASCENDING) {
                    mLCQuery.addAscendingOrder(item.getField());
                } else if (item.getDirection() == OrderBy.Direction.DESCENDING) {
                    mLCQuery.addDescendingOrder(item.getField());
                }
            }
        }

        return mLCQuery;
    }

    @Override
    public void delete(CollectionReference reference, SyncManager.Callback callback) {
        String collectionKey = reference.getKey();
        Query mQuery = reference.getQuery();
        LCQuery<LCObject> mLCQuery = createLCQuery(collectionKey, mQuery);
        mLCQuery.deleteAllInBackground()
                .subscribe(new Observer<LCNull>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull LCNull LCNull) {
                        callback.onSuccess();
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        callback.onFail(new AgoraException(e));
                    }

                    @Override
                    public void onComplete() {

                    }
                });
    }

    @Override
    public void update(DocumentReference reference, String key, Object data, SyncManager.DataItemCallback callback) {
        if (reference instanceof RoomReference) {
            LCObject mLCQuery = LCObject.createWithoutData(AgoraRoom.TABLE_NAME, reference.getId());
            mLCQuery.put(key, data);
            mLCQuery.saveInBackground()
                    .subscribe(new Observer<LCObject>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull LCObject LCObject) {
                            callback.onSuccess(new AgoraObject(LCObject));
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            callback.onFail(new AgoraException(e));
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        } else {
            String collectionKey = reference.getParent().getKey();
            LCObject mLCObjectCollection = LCObject.createWithoutData(collectionKey, reference.getId());
            mLCObjectCollection.put(key, data);
            mLCObjectCollection.saveInBackground()
                    .subscribe(new Observer<LCObject>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull LCObject LCObject) {
                            callback.onSuccess(new AgoraObject(LCObject));
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            callback.onFail(new AgoraException(e));
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        }
    }

    @Override
    public void update(DocumentReference reference, HashMap<String, Object> datas, SyncManager.DataItemCallback callback) {
        if (reference instanceof RoomReference) {
            LCObject mLCObject = LCObject.createWithoutData(AgoraRoom.TABLE_NAME, reference.getId());
            for (Map.Entry<String, Object> entry : datas.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                if (value instanceof DocumentReference) {
                    CollectionReference referenceParent = ((DocumentReference) value).getParent();
                    LCObject LCObjectItem = mLCObject.createWithoutData(referenceParent.getKey(), ((DocumentReference) value).getId());
                    mLCObject.put(key, LCObjectItem);
                } else {
                    mLCObject.put(key, value);
                }
            }
            mLCObject.saveInBackground()
                    .subscribe(new Observer<LCObject>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull LCObject LCObject) {
                            callback.onSuccess(new AgoraObject(LCObject));
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            callback.onFail(new AgoraException(e));
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        } else {
            String collectionKey = reference.getParent().getKey();
            LCObject mLCObjectCollection = LCObject.createWithoutData(collectionKey, reference.getId());
            for (Map.Entry<String, Object> entry : datas.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                if (value instanceof DocumentReference) {
                    CollectionReference referenceParent = ((DocumentReference) value).getParent();
                    LCObject LCObjectItem = LCObject.createWithoutData(referenceParent.getKey(), ((DocumentReference) value).getId());
                    mLCObjectCollection.put(key, LCObjectItem);
                } else {
                    mLCObjectCollection.put(key, value);
                }
            }
            mLCObjectCollection.saveInBackground()
                    .subscribe(new Observer<LCObject>() {
                        @Override
                        public void onSubscribe(@NonNull Disposable d) {

                        }

                        @Override
                        public void onNext(@NonNull LCObject LCObject) {
                            callback.onSuccess(new AgoraObject(LCObject));
                        }

                        @Override
                        public void onError(@NonNull Throwable e) {
                            callback.onFail(new AgoraException(e));
                        }

                        @Override
                        public void onComplete() {

                        }
                    });
        }
    }

    private HashMap<SyncManager.EventListener, LCLiveQuery> events = new HashMap<>();

    @Override
    public void subcribe(DocumentReference reference, SyncManager.EventListener listener) {
        if (reference instanceof RoomReference) {
            LCQuery<LCObject> query = createLCQuery(AgoraRoom.TABLE_NAME, reference.getQuery());
            LCLiveQuery mLCLiveQuery = LCLiveQuery.initWithQuery(query);
            mLCLiveQuery.setEventHandler(new LCLiveQueryEventHandler() {

                @Override
                public void onObjectCreated(LCObject LCObject) {
                    super.onObjectCreated(LCObject);
                    listener.onCreated(new AgoraObject(LCObject));
                }

                @Override
                public void onObjectUpdated(LCObject LCObject, List<String> updatedKeys) {
                    super.onObjectUpdated(LCObject, updatedKeys);
                    listener.onUpdated(new AgoraObject(LCObject));
                }

                @Override
                public void onObjectDeleted(String objectId) {
                    super.onObjectDeleted(objectId);
                    listener.onDeleted(objectId);
                }
            });

            events.put(listener, mLCLiveQuery);
            mLCLiveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() {
                @Override
                public void done(LCException e) {
                    if (null != e) {
                        if (e.getCode() == LCException.EXCEEDED_QUOTA) {
                            listener.onSubscribeError(new AgoraException(AgoraException.ERROR_LEANCLOULD_OVER_COUNT, e.getMessage()));
                        } else {
                            listener.onSubscribeError(new AgoraException(AgoraException.ERROR_LEANCLOULD_DEFAULT, e.getMessage()));
                        }
                    } else {
                    }
                }
            });
        } else {
            String collectionKey = reference.getParent().getKey();
            LCQuery<LCObject> query = createLCQuery(collectionKey, reference.getQuery());
            LCLiveQuery mLCLiveQuery = LCLiveQuery.initWithQuery(query);
            mLCLiveQuery.setEventHandler(new LCLiveQueryEventHandler() {

                @Override
                public void onObjectCreated(LCObject LCObject) {
                    super.onObjectCreated(LCObject);
                    listener.onCreated(new AgoraObject(LCObject));
                }

                @Override
                public void onObjectUpdated(LCObject LCObject, List<String> updatedKeys) {
                    super.onObjectUpdated(LCObject, updatedKeys);
                    listener.onUpdated(new AgoraObject(LCObject));
                }

                @Override
                public void onObjectDeleted(String objectId) {
                    super.onObjectDeleted(objectId);
                    listener.onDeleted(objectId);
                }
            });

            events.put(listener, mLCLiveQuery);
            mLCLiveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() {
                @Override
                public void done(LCException e) {
                    if (null != e) {
                        if (e.getCode() == LCException.EXCEEDED_QUOTA) {
                            listener.onSubscribeError(new AgoraException(AgoraException.ERROR_LEANCLOULD_OVER_COUNT, e.getMessage()));
                        } else {
                            listener.onSubscribeError(new AgoraException(AgoraException.ERROR_LEANCLOULD_DEFAULT, e.getMessage()));
                        }
                    } else {
                    }
                }
            });
        }
    }

    @Override
    public void subcribe(CollectionReference reference, SyncManager.EventListener listener) {
        String collectionKey = reference.getKey();
        LCQuery<LCObject> query = createLCQuery(collectionKey, reference.getQuery());
        LCLiveQuery mLCLiveQuery = LCLiveQuery.initWithQuery(query);
        mLCLiveQuery.setEventHandler(new LCLiveQueryEventHandler() {

            @Override
            public void onObjectCreated(LCObject LCObject) {
                super.onObjectCreated(LCObject);
                listener.onCreated(new AgoraObject(LCObject));
            }

            @Override
            public void onObjectUpdated(LCObject LCObject, List<String> updatedKeys) {
                super.onObjectUpdated(LCObject, updatedKeys);
                listener.onUpdated(new AgoraObject(LCObject));
            }

            @Override
            public void onObjectDeleted(String objectId) {
                super.onObjectDeleted(objectId);
                listener.onDeleted(objectId);
            }
        });

        events.put(listener, mLCLiveQuery);
        mLCLiveQuery.subscribeInBackground(new LCLiveQuerySubscribeCallback() {
            @Override
            public void done(LCException e) {
                if (null != e) {
                    if (e.getCode() == LCException.EXCEEDED_QUOTA) {
                        listener.onSubscribeError(new AgoraException(AgoraException.ERROR_LEANCLOULD_OVER_COUNT, e.getMessage()));
                    } else {
                        listener.onSubscribeError(new AgoraException(AgoraException.ERROR_LEANCLOULD_DEFAULT, e.getMessage()));
                    }
                } else {

                }
            }
        });
    }

    @Override
    public void unsubcribe(SyncManager.EventListener listener) {
        if (events.get(listener) != null) {
            events.get(listener).unsubscribeInBackground(new LCLiveQuerySubscribeCallback() {
                @Override
                public void done(LCException e) {

                }
            });
        }
    }
}
